/*!
 * @description
 * Класс для работы с локальными хранилищами на стороне клиента.
 * @requires SWFObject v2.2 (http://code.google.com/p/swfobject/)
 * @requires JSON object (http://www.JSON.org/js.html)
 * @author Dmitry Shkinev (berkel@yandex.ru)
 * @link http://code.google.com/p/ilocalstorage/
 * @version 0.1.0
 */

/**
 *
 */
var iLocalStorage = function()
{
	/** @type {Array} */
	var _readyCallbacks = [],

	/** @type {String} */
	_prefix = 'LS',

	/** @type {String} */
	_defaultNS = 'default',

	/** @type {Boolean} */
	_initialized = false,

	/** @type {Object} */
	_currentEngine = null,

	/** @type {Object} */
	_engines = {},

	/** @type {Object} */
	_availableEngines = {},

	/** @type {Object} */
	_currentCache = {},

	/** @type {Object} */
	_caches = {},

	/** @type {Number} */
	_loadedEnglinesCount = 0;

	/**
	 *
	 * @param {String} key
	 * @param {String} [ns]
	 * @return {String}
	 */
	function prepareKey(key, ns)
	{
		return _prefix + '_' + ns + '_' + key;
	}

	/**
	 *
	 */
	function initialize()
	{
		if (!_initialized)
		{
			var types = _options.engines, type, engine;

			for (var i = 0, l = types.length; i<l; i++)
			{
				type = types[i];
				engine = _engines[type];

				if (engine && engine.isAvailable())
				{
					_availableEngines[type] = engine;
					_loadedEnglinesCount++;
				}
			}

			for (type in _availableEngines)
				_availableEngines[type].initialize(_result.draw);

			_initialized = true;
		}
	}

	/**
	 *
	 * @param {Object} engine
	 * @return {Object}
	 */
	function getData(engine)
	{
		var result = {}, data = engine.getAll(), parts, fullKey, key, ns, item;

		for (fullKey in data)
		{
			if (0 === fullKey.indexOf(_prefix))
			{
				parts = fullKey.split('_');

				if ((ns = parts[1]) && (key = parts[2]))
				{
					if (!result[ns])
						result[ns] = {};

					item = JSON.parse(data[fullKey]);

					if (item.expires && item.expires < +new Date)
					{
						engine.remove(fullKey);
					}
					else
					{
						result[ns][key] = item;
					}
				}
			}
		}
		return result;
	}

	var _result = {

		/** @type {Boolean} */
		isReady: false,

		/** @type {Number} */
		type: null,

		/**
		 *
		 * @param {Number} type
		 * @return {Boolean}
		 */
		use: function(type)
		{
			var engine = _availableEngines[type];
			if (engine)
			{
				if (!_caches[type])
					_caches[type] = getData(engine);
				_currentCache = _caches[type];
				_currentEngine = engine;
				_result.type = type;
				return true;
			}
			return false;
		},

		/**
		 *
		 */
		draw: function()
		{
			if (!_result.isReady)
			{
				if (0 === --_loadedEnglinesCount)
				{
					var type = _options.defaultEngine;

					if (undefined === _availableEngines[type])
						for (type in _availableEngines) break;

					if (_availableEngines[type])
					{
						type = parseInt(type);

						_result.use(type);
						_result.isReady = true;

						for (var i = 0, l = _readyCallbacks.length; i<l; i++)
							_readyCallbacks[i].call(_result);
						_readyCallbacks = null;
					}
				}
			}
		},

		/**
		 *
		 * @param {Function} callback
		 */
		ready: function(callback)
		{
			if (_result.isReady)
			{
				callback.call(_result);
			}
			else
			{
				swfobject.addDomLoadEvent(initialize);
				_readyCallbacks.push(callback);
			}
		},

		/**
		 *
		 * @param {String} key
		 * @param {Object} value
		 * @param {String} [ns]
		 * @param {Number} [expires]
		 */
		set: function(key, value, ns, expires)
		{
			ns = ns || _defaultNS;

			var data = {'value': value};
			if (expires)
				data.expires = expires;
			_currentEngine.set(prepareKey(key, ns), JSON.stringify(data));

			if (!_currentCache[ns])
				_currentCache[ns] = {};
			_currentCache[ns][key] = data;
		},

		/**
		 *
		 * @param {String} key
		 * @param {String} [ns]
		 * @return {Object}
		 */
		get: function(key, ns)
		{
			ns = ns || _defaultNS;

			if (_currentCache[ns])
			{
				var item = _currentCache[ns][key];
				if (item)
				{
					if (item.expires && item.expires < +new Date)
					{
						_currentEngine.remove(prepareKey(key, ns));
					}
					else
					{
						return item.value;
					}
				}
			}
			return null;
		},

		/**
		 *
		 * @param {String} key
		 * @param {String} [ns]
		 */
		remove: function(key, ns)
		{
			ns = ns || _defaultNS;

			_currentEngine.remove(prepareKey(key, ns));

			if (_currentCache[ns])
				delete _currentCache[ns][key];
		},

		/**
		 *
		 * @param {String} key
		 * @param {String} [ns]
		 * @return {Boolean}
		 */
		hasKey: function(key, ns)
		{
			ns = ns || _defaultNS;

			return _currentCache[ns] && undefined !==_currentCache[ns][key];
		},

		/**
		 *
		 * @param {String} [ns]
		 * @return {Array}
		 */
		getKeys: function(ns)
		{
			ns = ns || _defaultNS;

			var result = [];
			if (_currentCache[ns])
			{
				for (var key in _currentCache[ns])
					result.push(key);
			}
			return result;
		},

		/**
		 *
		 * @return {Array}
		 */
		getNamespaces: function()
		{
			var result = [];
			for (var key in _currentCache)
				result.push(key);
			return result;
		},

		/**
		 *
		 * @param {String} [ns]
		 */
		clear: function(ns)
		{
			ns = ns || _defaultNS;

			if (_currentCache[ns])
			{
				for (var key in _currentCache[ns])
					_currentEngine.remove(prepareKey(key, ns));
				delete _currentCache[ns];
			}
		},

		/**
		 *
		 * @param {Object} options
		 */
		setup: function(options)
		{
			for (var key in options)
				_options[key] = options[key];
		}
	};

	_result.ENGINE_DOMSTORAGE = 1;
	_result.ENGINE_DATABASE = 2;
	_result.ENGINE_USERDATA = 3;
	_result.ENGINE_FLASH = 4;
	_result.ENGINE_COOKIE = 5;

	/** @type {Object} */
	var _options = {
		/** @type {Array} */
		'engines': [
			_result.ENGINE_DOMSTORAGE,
			_result.ENGINE_DATABASE,
			_result.ENGINE_USERDATA,
			_result.ENGINE_FLASH,
			_result.ENGINE_COOKIE
		],
		/** @type {Number} */
		'defaultEngine': null,
		/** @type {String} */
		'swfUrl': '/flash/ilocalstorage.swf',
		/** @type {String} */
		'cookieDomain': null,
		/** @type {String} */
		'cookiePath': null,
		/** @type {Number} */
		'cookieExpires': +new Date + 31104000000 // 12 * 30 * 24 * 60 * 60 * 1000
	};

	/**
	 *
	 */
	_engines[_result.ENGINE_DOMSTORAGE] = {

		/** @type {Object} */
		storage: null,

		/**
		 *
		 * @return {Boolean}
		 */
		isAvailable: function()
		{
			return window.localStorage || window.globalStorage;
		},

		/**
		 *
		 * @param {Function} callback
		 */
		initialize: function(callback)
		{
			this.storage = window.localStorage ? window.localStorage : window.globalStorage[location.hostname];

			callback.call(null);
		},

		/**
		 *
		 * @param {String} key
		 * @param {Object} value
		 */
		set: function(key, value)
		{
			this.storage.setItem(key, value);
		},

		/**
		 *
		 * @param {String} key
		 */
		remove: function(key)
		{
			this.storage.removeItem(key);
		},

		/**
		 *
		 * @return {Object}
		 */
		getAll: function()
		{
			var result = {},
				key,
				data,
				storage = this.storage;

			for (var i = storage.length; i--; )
			{
				key = storage.key(i);
				data = storage.getItem(key);
				result[key] = (data && data.value) ? data.value : data;
			}
			return result;
		}
	};

	/**
	 *
	 */
	_engines[_result.ENGINE_FLASH] = {

		/** @type {Object} */
		storage: null,

		/** @type {String} */
		flashId: '__localStorageFlash',

		/** @type {String} */
		playerVersion: '8.0.0',

		/**
		 *
		 * @return {Boolean}
		 */
		isAvailable: function()
		{
			return swfobject.hasFlashPlayerVersion(this.playerVersion);
		},

		/**
		 *
		 */
		initialize: function()
		{
			var container = document.createElement('span');
			container.style.position = 'absolute';
			container.style.left = '-1000px';
			container.style.top = '-1000px';

			var inner = document.createElement('span');
			inner.id = this.flashId;

			container.appendChild(inner);
			document.body.appendChild(container);

			swfobject.embedSWF(_options.swfUrl, this.flashId, '1', '1', this.playerVersion, null, null, null, null, function(oThis)
			{
				return function(data)
				{
					if (data.ref)
					{
						oThis.storage = data.ref;
					}
				};
			}(this));
		},

		/**
		 *
		 * @param {String} key
		 * @param {Object} value
		 */
		set: function(key, value)
		{
			this.storage.set(key, value);
		},

		/**
		 *
		 * @param {String} key
		 */
		remove: function(key)
		{
			this.storage.remove(key);
		},

		/**
		 *
		 * @return {Object}
		 */
		getAll: function()
		{
			return this.storage.getAll();
		}
	};

	/**
	 *
	 */
	_engines[_result.ENGINE_DATABASE] = {

		/** @type {Object} */
		database: null,

		/** @type {String} */
		databaseName: 'LS',

		/** @type {String} */
		tableName: 'storage',

		/** @type {Object} */
		cache: {},

		/**
		 *
		 * @return {Boolean}
		 */
		isAvailable: function()
		{
			return window.openDatabase;
		},

		/**
		 *
		 * @param {Function} callback
		 */
		initialize: function(callback)
		{
			var tableName = this.tableName,
				cache = this.cache;

			this.database = window.openDatabase(this.databaseName, '1.0', this.databaseName);

			this.database.transaction(function(transaction)
			{
				transaction.executeSql('CREATE TABLE IF NOT EXISTS `' + tableName + '` (`key` TEXT, `value` TEXT, unique(`key`))');
			});

			var statementCallback = function(transaction, oResult)
			{
				var rows = oResult.rows;
				if (rows)
				{
					for (var item, i = rows.length; i--; )
					{
						item = rows.item(i);
						cache[item.key] = item.value;
					}
				}

				callback.call(null);
			};

			this.database.transaction(function(transaction)
			{
				transaction.executeSql('SELECT `key`, `value` FROM `' + tableName + '`', [], statementCallback);
			});
		},

		/**
		 *
		 * @param {String} key
		 * @param {String} value
		 */
		set: function(key, value)
		{
			var tableName = this.tableName;
			this.database.transaction(function(transaction)
			{
				transaction.executeSql('REPLACE INTO `' + tableName + '` (`key`, `value`) VALUES (?, ?)', [key, value]);
			});
		},

		/**
		 *
		 * @param {String} key
		 */
		remove: function(key)
		{
			var tableName = this.tableName;

			this.database.transaction(function(transaction)
			{
				transaction.executeSql('DELETE FROM `' + tableName + '` WHERE `key`=?', [key]);
			});
		},

		/**
		 *
		 * @return {Object}
		 */
		getAll: function()
		{
			return this.cache;
		}
	};

	/**
	 *
	 */
	_engines[_result.ENGINE_USERDATA] = {

		/** @type {Object} */
		storage: null,

		/** @type {String} */
		ns: 'LS',

		/**
		 *
		 * @return {Boolean}
		 */
		isAvailable: function()
		{
			var userAgent = navigator.userAgent.toLowerCase();
			return /msie/.test(userAgent) && !/opera/.test(userAgent);
		},

		/**
		 *
		 * @param {Function} callback
		 */
		initialize: function(callback)
		{
			try
			{
				var link = document.createElement('link'),
					head = document.getElementsByTagName('head')[0];

				this.storage = head.appendChild(link);
				this.storage.addBehavior('#default#userData');
				this.storage.load(this.ns);

				callback.call(null);
			}
			catch (e)
			{
			}
		},

		/**
		 *
		 * @param {String} key
		 * @param {Object} value
		 */
		set: function(key, value)
		{
			try
			{
				this.storage.setAttribute(key, value);
				this.storage.save(this.ns);
			}
			catch (e)
			{
			}
		},

		/**
		 *
		 * @param {String} key
		 */
		remove: function(key)
		{
			this.storage.removeAttribute(key);
			this.storage.save(this.ns);
		},

		/**
		 *
		 * @return {Object}
		 */
		getAll: function()
		{
			var result = {},
				list = this.storage.XMLDocument.documentElement.attributes,
				key,
				storage = this.storage;

			for (var i = 0; i<list.length; i++)
			{
				key = list[i].name;
				result[key] = storage.getAttribute(key);
			}
			return result;
		}
	};

	/**
	 *
	 */
	_engines[_result.ENGINE_COOKIE] = {

		/**
		 *
		 * @return {Boolean}
		 */
		isAvailable: function()
		{
			return true;
		},

		/**
		 *
		 * @param {Function} callback
		 */
		initialize: function(callback)
		{
			callback.call(null);
		},

		/**
		 *
		 * @param {String} key
		 * @param {Object} value
		 */
		set: function(key, value)
		{
			var data = [key + '=' + encodeURIComponent(value)];
			0 !== _options.cookieExpires && data.push('expires=' + (new Date(_options.cookieExpires)).toUTCString());
			null !== _options.cookiePath && data.push('path=' + _options.cookiePath);
			null !== _options.cookieDomain && data.push('domain=' + _options.cookieDomain);
			document.cookie = data.join('; ');
		},

		/**
		 *
		 * @param {String} key
		 */
		remove: function(key)
		{
			var data = [key + "=''", 'expires=' + (new Date(+new Date - 1)).toUTCString()];
			null !== _options.cookiePath && data.push('path=' + _options.cookiePath);
			null !== _options.cookieDomain && data.push('domain=' + _options.cookieDomain);
			document.cookie = data.join('; ');
		},

		/**
		 *
		 * @return {Object}
		 */
		getAll: function()
		{
			var result = {},
				cookies = document.cookie.split('; '),
				parts;

			for (var i = 0, l = cookies.length; i<l; i++)
			{
				parts = cookies[i].split('=');
				if (parts.length === 2)
					result[parts[0]] = decodeURIComponent(parts[1]);
			}
			return result;
		}
	};

	return _result;
}();
